package com.hqd.ch03.v50.boot.web.servlet.server;

import com.hqd.ch03.utils.ClassUtils;
import com.hqd.ch03.v50.boot.web.server.AbstractConfigurableWebServerFactory;
import com.hqd.ch03.v50.boot.web.server.MimeMappings;
import com.hqd.ch03.v50.boot.web.servlet.ServletContextInitializer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.SessionCookieConfig;
import java.io.File;
import java.net.URL;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.*;

public abstract class AbstractServletWebServerFactory extends AbstractConfigurableWebServerFactory
        implements ConfigurableServletWebServerFactory {

    protected final Log logger = LogFactory.getLog(getClass());
    private final DocumentRoot documentRoot = new DocumentRoot(this.logger);
    private final StaticResourceJars staticResourceJars = new StaticResourceJars();
    private final Set<String> webListenerClassNames = new HashSet<>();
    private String contextPath = "";
    private String displayName;
    private Session session = new Session();
    private boolean registerDefaultServlet = false;
    private MimeMappings mimeMappings = new MimeMappings(MimeMappings.DEFAULT);
    private List<ServletContextInitializer> initializers = new ArrayList<>();
    private Jsp jsp = new Jsp();
    private Map<Locale, Charset> localeCharsetMappings = new HashMap<>();
    private Map<String, String> initParameters = Collections.emptyMap();
    private List<CookieSameSiteSupplier> cookieSameSiteSuppliers = new ArrayList<>();

    public AbstractServletWebServerFactory() {
    }

    public AbstractServletWebServerFactory(int port) {
        super(port);
    }

    public AbstractServletWebServerFactory(String contextPath, int port) {
        super(port);
        checkContextPath(contextPath);
        this.contextPath = contextPath;
    }

    public String getContextPath() {
        return this.contextPath;
    }

    @Override
    public void setContextPath(String contextPath) {
        checkContextPath(contextPath);
        this.contextPath = contextPath;
    }

    private void checkContextPath(String contextPath) {
        if (!contextPath.isEmpty()) {
            if ("/".equals(contextPath)) {
                throw new IllegalArgumentException("Root ContextPath must be specified using an empty string");
            }
            if (!contextPath.startsWith("/") || contextPath.endsWith("/")) {
                throw new IllegalArgumentException("ContextPath must start with '/' and not end with '/'");
            }
        }
    }

    public String getDisplayName() {
        return this.displayName;
    }

    @Override
    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

    public boolean isRegisterDefaultServlet() {
        return this.registerDefaultServlet;
    }

    @Override
    public void setRegisterDefaultServlet(boolean registerDefaultServlet) {
        this.registerDefaultServlet = registerDefaultServlet;
    }

    public MimeMappings getMimeMappings() {
        return this.mimeMappings;
    }

    @Override
    public void setMimeMappings(MimeMappings mimeMappings) {
        this.mimeMappings = new MimeMappings(mimeMappings);
    }

    public File getDocumentRoot() {
        return this.documentRoot.getDirectory();
    }

    @Override
    public void setDocumentRoot(File documentRoot) {
        this.documentRoot.setDirectory(documentRoot);
    }

    @Override
    public void setInitializers(List<? extends ServletContextInitializer> initializers) {
        this.initializers = new ArrayList<>(initializers);
    }

    @Override
    public void addInitializers(ServletContextInitializer... initializers) {
        this.initializers.addAll(Arrays.asList(initializers));
    }

    public Jsp getJsp() {
        return this.jsp;
    }

    @Override
    public void setJsp(Jsp jsp) {
        this.jsp = jsp;
    }

    public Session getSession() {
        return this.session;
    }

    @Override
    public void setSession(Session session) {
        this.session = session;
    }

    public Map<Locale, Charset> getLocaleCharsetMappings() {
        return this.localeCharsetMappings;
    }

    @Override
    public void setLocaleCharsetMappings(Map<Locale, Charset> localeCharsetMappings) {
        this.localeCharsetMappings = localeCharsetMappings;
    }

    public Map<String, String> getInitParameters() {
        return this.initParameters;
    }

    @Override
    public void setInitParameters(Map<String, String> initParameters) {
        this.initParameters = initParameters;
    }

    @Override
    public void addCookieSameSiteSuppliers(CookieSameSiteSupplier... cookieSameSiteSuppliers) {
        this.cookieSameSiteSuppliers.addAll(Arrays.asList(cookieSameSiteSuppliers));
    }

    public List<CookieSameSiteSupplier> getCookieSameSiteSuppliers() {
        return this.cookieSameSiteSuppliers;
    }

    @Override
    public void setCookieSameSiteSuppliers(List<? extends CookieSameSiteSupplier> cookieSameSiteSuppliers) {
        this.cookieSameSiteSuppliers = new ArrayList<>(cookieSameSiteSuppliers);
    }

    protected final ServletContextInitializer[] mergeInitializers(ServletContextInitializer... initializers) {
        List<ServletContextInitializer> mergedInitializers = new ArrayList<>();
        mergedInitializers.add((servletContext) -> this.initParameters.forEach(servletContext::setInitParameter));
        mergedInitializers.add(new SessionConfiguringInitializer(this.session));
        mergedInitializers.addAll(Arrays.asList(initializers));
        mergedInitializers.addAll(this.initializers);
        return mergedInitializers.toArray(new ServletContextInitializer[0]);
    }

    protected boolean shouldRegisterJspServlet() {
        return this.jsp != null && this.jsp.getRegistered()
                && ClassUtils.isPresent(this.jsp.getClassName(), getClass().getClassLoader());
    }

    protected final File getValidDocumentRoot() {
        return this.documentRoot.getValidDirectory();
    }

    protected final List<URL> getUrlsOfJarsWithMetaInfResources() {
        return this.staticResourceJars.getUrls();
    }

    protected final File getValidSessionStoreDir() {
        return getValidSessionStoreDir(true);
    }

    protected final File getValidSessionStoreDir(boolean mkdirs) {
        return this.session.getSessionStoreDirectory().getValidDirectory(mkdirs);
    }

    @Override
    public void addWebListeners(String... webListenerClassNames) {
        this.webListenerClassNames.addAll(Arrays.asList(webListenerClassNames));
    }

    protected final Set<String> getWebListenerClassNames() {
        return this.webListenerClassNames;
    }

    private static class SessionConfiguringInitializer implements ServletContextInitializer {

        private final Session session;

        SessionConfiguringInitializer(Session session) {
            this.session = session;
        }

        @Override
        public void onStartup(ServletContext servletContext) throws ServletException {
            if (this.session.getTrackingModes() != null) {
                servletContext.setSessionTrackingModes(unwrap(this.session.getTrackingModes()));
            }
            configureSessionCookie(servletContext.getSessionCookieConfig());
        }

        private void configureSessionCookie(SessionCookieConfig config) {
            Session.Cookie cookie = this.session.getCookie();
            com.hqd.ch03.v50.boot.context.properties.PropertyMapper map = com.hqd.ch03.v50.boot.context.properties.PropertyMapper.get().alwaysApplyingWhenNonNull();
            map.from(cookie::getName).to(config::setName);
            map.from(cookie::getDomain).to(config::setDomain);
            map.from(cookie::getPath).to(config::setPath);
            map.from(cookie::getComment).to(config::setComment);
            map.from(cookie::getHttpOnly).to(config::setHttpOnly);
            map.from(cookie::getSecure).to(config::setSecure);
            map.from(cookie::getMaxAge).asInt(Duration::getSeconds).to(config::setMaxAge);
        }

        private Set<javax.servlet.SessionTrackingMode> unwrap(Set<Session.SessionTrackingMode> modes) {
            if (modes == null) {
                return null;
            }
            Set<javax.servlet.SessionTrackingMode> result = new LinkedHashSet<>();
            for (Session.SessionTrackingMode mode : modes) {
                result.add(javax.servlet.SessionTrackingMode.valueOf(mode.name()));
            }
            return result;
        }

    }

}

